/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Google Calendar Provider code.
 *
 * The Initial Developer of the Original Code is
 *   Philipp Kewisch <mozilla@kewis.ch>
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s): Josh Bohde, Natalia Smirnova, Tory Cheatham
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

function googleRequest(){
	this.mQueryParameters = new Array();
}

googleRequest.prototype = {
	 /* Members */
    mMethod: null,
    mUriString: null,
    mUploadContent: null,
    mUploadData: null,
    mResponseListenerObject: null,
    mResponseListenerFunc: null,
    mToken: null,
    mExtraData: null,
    mQueryParameters: null,
    mType: null,
    mCalendar: null,
	mGoogleUser: null,
	mGooglePass: null,
	
	 /* Constants */
    LOGIN: 1,
    GET: 2,
    ADD: 3,
    MODIFY: 4,
    DELETE: 5,
	
	QueryInterface: function cGR_QueryInterface(aIID) {
        if (!aIID.equals(Components.interfaces.nsISupports) &&
            !aIID.equals(Components.interfaces.nsIStreamLoaderObserver) &&
            !aIID.equals(Components.interfaces.nsIDocShellTreeItem) &&
            !aIID.equals(Components.interfaces.nsIInterfaceRequestor) &&
            !aIID.equals(Components.interfaces.nsIChannelEventSink) &&
            !aIID.equals(Components.interfaces.nsIProgressEventSink) &&
            !aIID.equals(Components.interfaces.nsIHttpEventSink) &&
            !aIID.equals(Components.interfaces.nsIStreamListener)) {
                throw Components.results.NS_ERROR_NO_INTERFACE;
        }
        return this;
    },
	
	/**
     * attribute type
     * The type of this reqest. Must be one of
     * LOGIN, GET, ADD, MODIFY, DELETE
     * This also sets the Request Method and for the LOGIN request also the uri
     */
    get type() { return this.mType; },

    set type(v) {
        switch (v) {
            case this.LOGIN:
                this.mMethod = "POST";
                this.mUriString = "https://www.google.com/accounts/ClientLogin";
                break;
            case this.META:
                // Fall through
            case this.GET:
                this.mMethod = "GET";
                break;
            case this.ADD:
                this.mMethod = "POST";
                break;
            case this.MODIFY:
                this.mMethod = "PUT";
                break;
            case this.DELETE:
                this.mMethod = "DELETE";
                break;
            default:
                throw new Components.Exception("", Components.results.NS_ERROR_ILLEGAL_VALUE);
                break;
        }
        this.mType = v;
        return v;
    },
	 /**
     * attribute method
     * The Request Method to use (GET, POST, PUT, DELETE)
     */
    get method() {
        return this.mMethod;
    },
    set method(v) {
        return this.mMethod = v;
    },

    /**
     * attribute uri
     * The String representation of the uri to be requested
     */
    get uri() {
        return this.mUriString;
    },
    set uri(v) {
        return this.mUriString = v;
    },

    /**
     * attribute googleUser
     * The Username
     */
    get googleUser() {
        return this.mGoogleUser;
    },
    set mGoogleUser(v) {
        return this.mGoogleUser= v;
    },
	
	/**
     * attribute googlePass
     * The Password
     */
    get googlePass() {
        return this.mGooglePass;
    },
    set mGoogleUser(v) {
        return this.mGooglePass= v;
    },
	
	/**
     * setResponseListener
     * The Function and Object of the listener to be called on complete
     * operation.
     *
     * @param aObj    The object for the callback
     * @param aFunc   The function for the callback
     */
    setResponseListener: function cGR_setResponseListener(aObj, aFunc) {
        this.mResponseListenerObject = aObj;
        this.mResponseListenerFunc = aFunc;
    },

    /**
     * setUploadData
     * The HTTP body data for a POST or PUT request.
     *
     * @param aContentType The Content type of the Data
     * @param aData        The Data to upload
     */
    setUploadData: function cGR_setUploadData(aContentType, aData) {
        this.mUploadContent = aContentType;
        this.mUploadData = aData;
        if (this.mType == this.LOGIN) {
            LOG("Setting upload data for login request (hidden)");
        } else {
            LOG({action:"Setting Upload Data:",
                 content:aContentType,
                 data:aData});
        }
    },

    /**
     * attribute extraData
     * Extra Data to save with the request
     */
    get extraData() {
        return this.mExtraData;
    },
    set extraData(v) {
        return this.mExtraData = v;
    },

    /**
     * attribute calendar
     * The calendar that initiated this request. Will be used by the login
     * response function to determine how and where to set passwords.
     */
    get calendar() {
        return this.mCalendar;
    },
    set calendar(v) {
        return this.mCalendar = v;
    },

    /**
     * addQueryParameter
     * Adds a query parameter to this request. This will be used in conjunction
     * with the uri.
     *
     * @param aKey      The key of the request parameter.
     * @param aValue    The value of the request parameter. This parameter will
     *                  be escaped.
     */
    addQueryParameter: function cGR_addQueryParameter(aKey, aValue) {
        if (aValue == null || aValue == "") {
            // Silently ignore empty values.
            return;
        }
        this.mQueryParameters.push(aKey + "=" + encodeURIComponent(aValue));
    },

    /**
     * commit
     * Starts the request process. This can be called multiple times if the
     * request should be repeated
     *
     * @param aSession  The session object this request should be made with.
     *                  This parameter is optional
     */
    commit: function cGR_commit() {

        try {
            
			// create the channel
            var ioService = Components.classes["@mozilla.org/network/io-service;1"].
                            getService(Components.interfaces.nsIIOService);

            var uristring = this.mUriString;
            if (this.mQueryParameters.length > 0) {
                uristring += "?" + this.mQueryParameters.join("&");
            }
            var uri = ioService.newURI(uristring, null, null);
            var channel = ioService.newChannelFromURI(uri);

            this.prepareChannel(channel);

            channel = channel.QueryInterface(Components.interfaces.nsIHttpChannel);
            channel.redirectionLimit = 3;

            var streamLoader = Components.classes["@mozilla.org/network/stream-loader;1"].
                               createInstance(Components.interfaces.nsIStreamLoader);

            LOG("calGoogleRequest: Requesting " + this.mMethod + " " +
                channel.URI.spec);

            channel.notificationCallbacks = this;

            // Required to be trunk and branch compatible.
            if (Components.interfaces.nsIStreamLoader.number ==
                "{31d37360-8e5a-11d3-93ad-00104ba0fd40}") {
                streamLoader.init(channel, this, this);
            } else if (Components.interfaces.nsIStreamLoader.number ==
                      "{8ea7e890-8211-11d9-8bde-f66bad1e3f3a}") {
                streamLoader.init(this);
                channel.asyncOpen(streamLoader, this);
            }
        } catch (e) {
            // Let the response function handle the error that happens here
            this.fail(e.result, e.message);
        }
    },

    /**
     * fail
     * Call this request's listener with the given code and Message
     *
     * @param aCode     The Error code to fail with
     * @param aMessage  The Error message. If this is null, an error Message
     *                  from calIGoogleErrors will be used.
     */
    fail: function cGR_fail(aCode, aMessage) {
        this.mResponseListenerFunc.call(this.mResponseListenerObject,
                                        this,
                                        aCode,
                                        aMessage);
    },

    /**
     * succeed
     * Call this request's listener with a Success Code and the given Result.
     *
     * @param aResult   The result Text of this request.
     */
    succeed: function cGR_succeed(aResult) {
        // Succeeding is nothing more than failing with the result code set to
        // NS_OK.
        this.fail(Components.results.NS_OK, aResult);
    },

    /**
     * prepareChannel
     * Prepares the passed channel to match this objects properties
     *
     * @param aChannel    The Channel to be prepared
     */
    prepareChannel: function cGR_prepareChannel(aChannel) {

        // No caching
        aChannel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE;

        // Set upload Data
        if (this.mUploadData) {
            var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
                            createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
            converter.charset = "UTF-8";

            var stream = converter.convertToInputStream(this.mUploadData);
            aChannel = aChannel.QueryInterface(Components.interfaces.nsIUploadChannel);
            aChannel.setUploadStream(stream, this.mUploadContent, -1);
        }

        aChannel  = aChannel.QueryInterface(Components.interfaces.nsIHttpChannel);

        // Depending on the preference, we will use X-HTTP-Method-Override to
        // get around some proxies. This will default to true.
        if (getPrefSafe("calendar.google.useHTTPMethodOverride", true) &&
            (this.mMethod == "PUT" || this.mMethod == "DELETE")) {

            aChannel.requestMethod = "POST";
            aChannel.setRequestHeader("X-HTTP-Method-Override",
                                      this.mMethod,
                                      false);
            if (this.mMethod == "DELETE") {
                // DELETE has no body, set an empty one so that Google accepts
                // the request.
                aChannel.setRequestHeader("Content-Type",
                                          "application/atom+xml; charset=UTF-8",
                                          false);
                aChannel.setRequestHeader("Content-Length", 0, false);
            }
        } else {
            aChannel.requestMethod = this.mMethod;
        }

        // Add Authorization
        if (this.mToken) {
            aChannel.setRequestHeader("Authorization",
                                      "GoogleLogin auth="
                                      +  this.mToken,
                                      false);
        }
    },

    /**
     * @see nsIInterfaceRequestor
     */
    getInterface: function cGR_getInterface(aIID) {
        try {
            return this.QueryInterface(aIID);
        } catch (e) {
            throw Components.results.NS_NOINTERFACE;
        }
    },

    /**
     * @see nsIChannelEventSink
     */
    onChannelRedirect: function cGR_onChannelRedirect(aOldChannel,
                                                      aNewChannel,
                                                      aFlags) {
        // all we need to do to the new channel is the basic preparation
        this.prepareChannel(aNewChannel);
    },

    /**
     * @see nsIHttpEventSink
     * (not implementing will cause annoying exceptions)
     */
    onRedirect: function cGR_onRedirect(aOldChannel, aNewChannel) { },

    /**
     * @see nsIProgressEventSink
     * (not implementing will cause annoying exceptions)
     */
    onProgress: function cGR_onProgress(aRequest,
                                        aContext,
                                        aProgress,
                                        aProgressMax) { },

    onStatus: function cGR_onStatus(aRequest,
                                    aContext,
                                    aStatus,
                                    aStatusArg) { },

    /**
     * @see nsIStreamLoaderObserver
     */
    onStreamComplete: function cGR_onStreamComplete(aLoader,
                                                    aContext,
                                                    aStatus,
                                                    aResultLength,
                                                    aResult) {
        if (!aResult || aStatus != Components.results.NS_OK) {
            this.fail(aStatus, aResult);
            return;
        }

        var httpChannel = aLoader.request.QueryInterface(Components.interfaces.nsIHttpChannel);
        var uploadChannel = aLoader.request.QueryInterface(Components.interfaces.nsIUploadChannel);

        // Convert the stream, falling back to utf-8 in case its not given.
        var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
                        createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
        converter.charset = httpChannel.contentCharset || "UTF-8";
        var result;
        try {
            result = converter.convertFromByteArray(aResult, aResultLength);
        } catch (e) {
            this.fail(Components.results.NS_ERROR_FAILURE,
                      "Could not convert bytestream to Unicode: " + e);
            return;
        }

        // Handle all (documented) error codes
        switch (httpChannel.responseStatus) {
            case 200: /* No error. */
            case 201: /* Creation of a resource was successful. */
                // Everything worked out, we are done
                this.succeed(result);
                break;

            case 401: /* Authorization required. */
            case 403: /* Unsupported standard parameter, or authentication or
                         Authorization failed. */
               // LOG("Login failed for " + this.mSession.googleUser +
                   //" HTTP Status " + httpChannel.responseStatus );

                // login failed. auth token must be invalid, password too

                if (this.type == this.MODIFY ||
                    this.type == this.DELETE ||
                    this.type == this.ADD) {
                    // Encountering this error on a write request means the
                    // calendar is readonly
                    this.fail(Components.interfaces.calIErrors.CAL_IS_READONLY, result);
                } else if (this.type == this.LOGIN) {
                    // If this was a login request itself, then fail it.
                    // That will take care of logging in again
                    //this.mSession.invalidate();
                    this.fail(kGOOGLE_LOGIN_FAILED, result);
                } else {
                    // Retry the request. Invalidating the session will trigger
                    // a new login dialog.
                    //this.mSession.invalidate();
                    //this.mSession.asyncItemRequest(this);
                }

                break;
            case 409: /* Specified version number doesn't match resource's
                         latest version number. */

                // 409 Conflict. The client should get a newer version of the
                // event
                // and edit that one.

                // TODO Enhancement tracked in bug 362645
                // Fall through, if 409 is not handled then the event is not
                // modified/deleted, which is definitly an error.
            default:
                // The following codes are caught here:
                //  400 BAD REQUEST: Invalid request URI or header, or
                //                   unsupported nonstandard parameter.
                //  404 NOT FOUND: Resource (such as a feed or entry) not found.
                //  500 INTERNAL SERVER ERROR: Internal error. This is the
                //                             default code that is used for
                //                             all unrecognized errors.
                //

                // Something else went wrong
                var error = "A request Error Occurred. Status Code: " +
                            httpChannel.responseStatus + " " +
                            httpChannel.responseStatusText + " Body: " +
                            result;

                this.fail(Components.results.NS_ERROR_FAILURE, error);
                break;
        }
    },
	
	loginComplete: function cGS_loginComplete(aStatus, aResult) {

        // About mLoggingIn: this should only be set to false when either
        // something went wrong or mAuthToken is set. This avoids redundant
        // logins to Google. Hence mLoggingIn is set three times in the course
        // of this function

        if (!aResult || aStatus != Components.results.NS_OK) {
            this.mLoggingIn = false;
            LOG("Login failed. Status: " + aStatus);

            if (aStatus == kGOOGLE_LOGIN_FAILED) {
                // If the login failed, then retry the login. This is not an
                // error that should trigger failing the calICalendar's request.
                // The login request's extraData contains the request object
                // that triggered the login initially
                this.loginAndContinue(aRequest.extraData);
            } else {
                LOG("Failing queue with " + aStatus);
                this.failQueue(aStatus);
            }
        } else {
            var start = aResult.indexOf("Auth=");
            if (start == -1) {
                // The Auth token could not be extracted
                this.mLoggingIn = false;
                this.invalidate();

                // Retry login
                this.loginAndContinue();
            } else {

                this.mToken = aResult.substring(start+5,
                                                    aResult.length - 1);
                this.mLoggingIn = false;

                /*if (this.savePassword) { //TODO
                    try {
                        passwordManagerSave(this.mGoogleUser,
                                            this.mGooglePass);
                    } catch (e) {
                        // This error is non-fatal, but would constrict
                        // functionality
                        LOG("Error adding password to manager");*/
                    }
                }
            }
        }
    },
	
	loginAndContinue: function cGS_loginAndContinue() {

        alert("Logging in!");
        try{
        /*if (this.mLoggingIn) {
            LOG("loginAndContinue called while logging in");
            return;
        }
        
        try {
            LOG("Logging in to " + this.mGoogleUser);

            // We need to have a user and should not be logging in
            ASSERT(!this.mLoggingIn);
            ASSERT(this.mGoogleUser);

            // Check if we have a password. If not, authentication may have
            // failed.
            if (!this.mGooglePass) {
                var username= { value: this.mGoogleUser };
                var password = { value: null };
                var savePassword = { value: false };

                // Try getting a new password, potentially switching sesssions.
                var calendarName = (aRequest.calendar ?
                                    aRequest.calendar.googleCalendarName :
                                    this.mGoogleUser);

                if (getCalendarCredentials(calendarName,
                                           username,
                                           password,
                                           savePassword)) {

                    LOG("Got the pw from the calendar credentials: " +
                        calendarName);

                    // If a different username was entered, switch sessions

                    if (aRequest.calendar &&
                        username.value != this.mGoogleUser) {

                        var newSession = getSessionByUsername(username.value);
                        newSession.googlePassword = password.value;
                        newSession.savePassword = savePassword.value;
                        setCalendarPref(aRequest.calendar,
                                        "googleUser",
                                        "CHAR",
                                        username.value);

                        // Set the new session for the calendar
                        /*aRequest.calendar.session = newSession;
                        LOG("Setting " + aRequest.calendar.name +
                            "'s Session to " + newSession.googleUser);

                        // Move all requests by this calendar to its new session
                        function cGS_login_moveToSession(element, index, arr) {
                            if (element.calendar == aRequest.calendar) {
                                LOG("Moving " + element.uri + " to " +
                                    newSession.googleUser);
                                newSession.asyncItemRequest(element);
                                return false;
                            }
                            return true;
                        }
                        this.mItemQueue = this.mItemQueue
                                          .filter(cGS_login_moveToSession);

                        // Do not complete the request here, since it has been
                        // moved. This is not an error though, so nothing is
                        // thrown.
                        return;
                    }

                    // If we arrive at this point, then the session was not
                    // changed. Just adapt the password from the dialog and
                    // continue.
                    this.mGooglePass = password.value;
                    this.savePassword = savePassword.value;
                } else {
                    LOG("Could not get any credentials for " +
                        calendarName + " (" +
                        this.mGoogleUser + ")");

                    // The User even canceled the login prompt asking for
                    // the user. This means we have to fail all requests
                    // that belong to that calendar and are in the queue. This
                    // will also include the request that initiated the login
                    // request, so that dosent need to be handled extra.
                    this.failQueue(Components.results.NS_ERROR_NOT_AVAILABLE,
                                   aRequest.calendar);

                    // Unset the session in the requesting calendar, if the user
                    // canceled the login dialog that also asks for the
                    // username, then the session is not valid. This also
                    // prevents multiple login windows.
                    if (aRequest.calendar)
                        aRequest.calendar.session = null;

                    return;
                }
            }

            // Now we should have a password
            //ASSERT(this.mGooglePass);

            // Start logging in
            //this.mLoggingIn = true;
			*/
            // Get Version info
            var appInfo = Components.classes["@mozilla.org/xre/app-info;1"].
                          getService(Components.interfaces.nsIXULAppInfo);
           
            var source = appInfo.vendor + "-" +
                         appInfo.name + "-" +
                         appInfo.version;

            // Request Login

            this.type = this.LOGIN;
            this.setResponseListener(this, this.loginComplete);
            this.setUploadData("application/x-www-form-urlencoded",
                                  "Email=" + encodeURIComponent(this.mGoogleUser) +
                                  "&Passwd=" + encodeURIComponent(this.mGooglePass) +
                                  "&accountType=HOSTED_OR_GOOGLE" +
                                  "&source=" + encodeURIComponent(source) +
                                  "&service=cl");
            this.asyncRawthis(this);
        } catch (e) {alert("Error logging in: " + e);}
    },
};